require 'rexml/text'

module Jshint::Reporters
  # Outputs a lint report in JUnit XML formt
  class Junit
    # @return [String] the report output
    attr_reader :output

    # Sets up the output string for the final report
    #
    # @param results [Hash] Key value pairs containing the filename and associated errors
    def initialize(results = {})
      @results = results
      @output = ''
    end

    # Loops through all the errors and generates the report
    #
    # @example
    #   foo/bar/baz.js: line 4, col 46, Bad operand.
    #   foo/bar/baz.js: line 39, col 7, Missing semicolon.
    #
    #   2 errors
    #
    # @return [String] The default report
    def report
      @output = <<-TEMPLATE
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="#{File.basename(Dir.pwd)}" timestamp="#{DateTime.now}">
TEMPLATE

      # Group according to the errors.
      error_groups = {}
      @results.each do |file, errors|
        errors.each do |error|
          next unless error && error['code']

          error_groups[error['code']] ||= []
          error_groups[error['code']] << {
            file: file,
            line: error['line'],
            character: error['character'],
            message: error['reason']
          }
        end
      end

      # Combine all the errors and the tests for which we have messages. Combine both together.
      all_codes = error_groups.keys + TESTS.keys

      all_codes.each do |code|
        print_errors_for_code(code, error_groups.fetch(code, []))
      end

      @output <<= <<-TEMPLATE
  </testsuite>
</testsuites>
TEMPLATE

      output
    end

    # Appends new error elements to the Report output
    #
    # @example
    #  <testcase classname="JUnitXmlReporter.constructor" name="should default path to an empty string" time="0.006">
    #    <failure message="test failure">Assertion failed</failure>
    # </testcase>
    #
    # @param code [String] The error code
    # @param errors [Array] The errors for the code
    # @return [void]
    def print_errors_for_code(code, errors)
      name = fetch_error_messages(code, errors)
      output << "    <testcase classname=\"jshint.#{code}\" name=\"#{escape(name)}\">\n"
      errors.each do |error|
        output << add_error_message(code, error)
      end
      output << "    </testcase>\n"
      output
    end

    # Escapes the text given for XML.
    #
    # @param text [String] The text to escape
    # @return [String]
    def escape(text)
      REXML::Text.new(text, false, nil, false).to_s
    end

    private

    def fetch_error_messages(code, errors)
      return '' if errors.empty?
      TESTS.fetch(code, errors.first[:message])
    end

    def add_error_message(code, error)
      output << "      <failure type=\"#{code}\" message=\"#{escape(error[:message])}\">"
      output << "%s, line %s, col %s\n" % [
        escape(error[:file]),
        error[:line].to_s,
        error[:character].to_s
      ]
      output << "\n</failure>\n"
    end

    TESTS = {
        E001: "Bad option: '{a}'.",
        E002: "Bad option value.",
        E003: "Expected a JSON value.",
        E004: "Input is neither a string nor an array of strings.",
        E005: "Input is empty.",
        E006: "Unexpected early end of program.",
        E007: "Missing \"use strict\" statement.",
        E008: "Strict violation.",
        E009: "Option 'validthis' can't be used in a global scope.",
        E010: "'with' is not allowed in strict mode.",
        E011: "const '{a}' has already been declared.",
        E012: "const '{a}' is initialized to 'undefined'.",
        E013: "Attempting to override '{a}' which is a constant.",
        E014: "A regular expression literal can be confused with '/='.",
        E015: "Unclosed regular expression.",
        E016: "Invalid regular expression.",
        E017: "Unclosed comment.",
        E018: "Unbegun comment.",
        E019: "Unmatched '{a}'.",
        E020: "Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.",
        E021: "Expected '{a}' and instead saw '{b}'.",
        E022: "Line breaking error '{a}'.",
        E023: "Missing '{a}'.",
        E024: "Unexpected '{a}'.",
        E025: "Missing ':' on a case clause.",
        E026: "Missing '}' to match '{' from line {a}.",
        E027: "Missing ']' to match '[' form line {a}.",
        E028: "Illegal comma.",
        E029: "Unclosed string.",
        E030: "Expected an identifier and instead saw '{a}'.",
        E031: "Bad assignment.",
        E032: "Expected a small integer or 'false' and instead saw '{a}'.",
        E033: "Expected an operator and instead saw '{a}'.",
        E034: "get/set are ES5 features.",
        E035: "Missing property name.",
        E036: "Expected to see a statement and instead saw a block.",
        E037: "Constant {a} was not declared correctly.",
        E038: "Variable {a} was not declared correctly.",
        E039: "Function declarations are not invocable. Wrap the whole function invocation in parens.",
        E040: "Each value should have its own case label.",
        E041: "Unrecoverable syntax error.",
        E042: "Stopping.",
        E043: "Too many errors.",
        E044: "'{a}' is already defined and can't be redefined.",
        E045: "Invalid for each loop.",
        E046: "A yield statement shall be within a generator function (with syntax: `function*`)",
        E047: "A generator function shall contain a yield statement.",
        E048: "Let declaration not directly within block.",
        E049: "A {a} cannot be named '{b}'.",
        W001: "'hasOwnProperty' is a really bad name.",
        W002: "Value of '{a}' may be overwritten in IE 8 and earlier.",
        W003: "'{a}' was used before it was defined.",
        W004: "'{a}' is already defined.",
        W005: "A dot following a number can be confused with a decimal point.",
        W006: "Confusing minuses.",
        W007: "Confusing pluses.",
        W008: "A leading decimal point can be confused with a dot: '{a}'.",
        W009: "The array literal notation [] is preferrable.",
        W010: "The object literal notation {} is preferrable.",
        W011: "Unexpected space after '{a}'.",
        W012: "Unexpected space before '{a}'.",
        W013: "Missing space after '{a}'.",
        W014: "Bad line breaking before '{a}'.",
        W015: "Expected '{a}' to have an indentation at {b} instead at {c}.",
        W016: "Unexpected use of '{a}'.",
        W017: "Bad operand.",
        W018: "Confusing use of '{a}'.",
        W019: "Use the isNaN function to compare with NaN.",
        W020: "Read only.",
        W021: "'{a}' is a function.",
        W022: "Do not assign to the exception parameter.",
        W023: "Expected an identifier in an assignment and instead saw a function invocation.",
        W024: "Expected an identifier and instead saw '{a}' (a reserved word).",
        W025: "Missing name in function declaration.",
        W026: "Inner functions should be listed at the top of the outer function.",
        W027: "Unreachable '{a}' after '{b}'.",
        W028: "Label '{a}' on {b} statement.",
        W030: "Expected an assignment or function call and instead saw an expression.",
        W031: "Do not use 'new' for side effects.",
        W032: "Unnecessary semicolon.",
        W033: "Missing semicolon.",
        W034: "Unnecessary directive \"{a}\".",
        W035: "Empty block.",
        W036: "Unexpected /*member '{a}'.",
        W037: "'{a}' is a statement label.",
        W038: "'{a}' used out of scope.",
        W039: "'{a}' is not allowed.",
        W040: "Possible strict violation.",
        W041: "Use '{a}' to compare with '{b}'.",
        W042: "Avoid EOL escaping.",
        W043: "Bad escaping of EOL. Use option multistr if needed.",
        W044: "Bad or unnecessary escaping.",
        W045: "Bad number '{a}'.",
        W046: "Don't use extra leading zeros '{a}'.",
        W047: "A trailing decimal point can be confused with a dot: '{a}'.",
        W048: "Unexpected control character in regular expression.",
        W049: "Unexpected escaped character '{a}' in regular expression.",
        W050: "JavaScript URL.",
        W051: "Variables should not be deleted.",
        W052: "Unexpected '{a}'.",
        W053: "Do not use {a} as a constructor.",
        W054: "The Function constructor is a form of eval.",
        W055: "A constructor name should start with an uppercase letter.",
        W056: "Bad constructor.",
        W057: "Weird construction. Is 'new' unnecessary?",
        W058: "Missing '()' invoking a constructor.",
        W059: "Avoid arguments.{a}.",
        W060: "document.write can be a form of eval.",
        W061: "eval can be harmful.",
        W062: "Wrap an immediate function invocation in parens " +
            "to assist the reader in understanding that the expression " +
            "is the result of a function, and not the function itself.",
        W063: "Math is not a function.",
        W064: "Missing 'new' prefix when invoking a constructor.",
        W065: "Missing radix parameter.",
        W066: "Implied eval. Consider passing a function instead of a string.",
        W067: "Bad invocation.",
        W068: "Wrapping non-IIFE function literals in parens is unnecessary.",
        W069: "['{a}'] is better written in dot notation.",
        W070: "Extra comma. (it breaks older versions of IE)",
        W071: "This function has too many statements. ({a})",
        W072: "This function has too many parameters. ({a})",
        W073: "Blocks are nested too deeply. ({a})",
        W074: "This function's cyclomatic complexity is too high. ({a})",
        W075: "Duplicate key '{a}'.",
        W076: "Unexpected parameter '{a}' in get {b} function.",
        W077: "Expected a single parameter in set {a} function.",
        W078: "Setter is defined without getter.",
        W079: "Redefinition of '{a}'.",
        W080: "It's not necessary to initialize '{a}' to 'undefined'.",
        W081: "Too many var statements.",
        W082: "Function declarations should not be placed in blocks. " +
            "Use a function expression or move the statement to the top of " +
            "the outer function.",
        W083: "Don't make functions within a loop.",
        W084: "Expected a conditional expression and instead saw an assignment.",
        W085: "Don't use 'with'.",
        W086: "Expected a 'break' statement before '{a}'.",
        W087: "Forgotten 'debugger' statement?",
        W088: "Creating global 'for' variable. Should be 'for (var {a} ...'.",
        W089: "The body of a for in should be wrapped in an if statement to filter " +
            "unwanted properties from the prototype.",
        W090: "'{a}' is not a statement label.",
        W091: "'{a}' is out of scope.",
        W092: "Wrap the /regexp/ literal in parens to disambiguate the slash operator.",
        W093: "Did you mean to return a conditional instead of an assignment?",
        W094: "Unexpected comma.",
        W095: "Expected a string and instead saw {a}.",
        W096: "The '{a}' key may produce unexpected results.",
        W097: "Use the function form of \"use strict\".",
        W098: "'{a}' is defined but never used.",
        W099: "Mixed spaces and tabs.",
        W100: "This character may get silently deleted by one or more browsers.",
        W101: "Line is too long.",
        W102: "Trailing whitespace.",
        W103: "The '{a}' property is deprecated.",
        W104: "'{a}' is only available in JavaScript 1.7.",
        W105: "Unexpected {a} in '{b}'.",
        W106: "Identifier '{a}' is not in camel case.",
        W107: "Script URL.",
        W108: "Strings must use doublequote.",
        W109: "Strings must use singlequote.",
        W110: "Mixed double and single quotes.",
        W112: "Unclosed string.",
        W113: "Control character in string: {a}.",
        W114: "Avoid {a}.",
        W115: "Octal literals are not allowed in strict mode.",
        W116: "Expected '{a}' and instead saw '{b}'.",
        W117: "'{a}' is not defined.",
        W118: "'{a}' is only available in Mozilla JavaScript extensions (use moz option).",
        W119: "'{a}' is only available in ES6 (use esnext option).",
        I001: "Comma warnings can be turned off with 'laxcomma'.",
        I002: "Reserved words as properties can be used under the 'es5' option.",
        I003: "ES5 option is now set per default"
    }.freeze
  end
end
